
const path = require('path')
const fs = require('fs')
const babylon = require('babylon') // 主要是把源码 转换成ast
const t = require('@babel/types') // 替换节点
const traverse = require('@babel/traverse').default // 遍历节点
const generator = require('@babel/generator').default // 生成替换的节点
const ejs = require('ejs')
const dirExists = require('./dirExists.js') // 判断文件是否存在，不存在则创建
const { SyncHook } = require('tapable')
module.exports = class {
  constructor(config) {
    this.config = config // webpack.config.js
    // 需要保存入口文件的路径
    this.entryId
    // 需要保存所有的模块依赖
    this.modules = {}
    // 入口路径
    this.entry = config.entry
    // 工作路径 当前运行命令的路径
    this.root = process.cwd() // 内置方法
    // 声明钩子
    this.hooks = {
      entryOption: new SyncHook(),
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      afterPlugins: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook(),
    }
    let plugins = this.config.plugins
    if (Array.isArray(plugins)) {
      plugins.forEach(plugin => {
        plugin.apply(this)
      })
    }
    this.hooks.afterPlugins.call()
  }
  // 读取源码
  getSource (modulePath) {
    let content =  fs.readFileSync(modulePath, 'utf8')
    // 匹配loader
    let rules = this.config.module ? this.config.module.rules || [] : []
    for (let i = 0; i < rules.length; i++) {
      let {test, use} = rules[i]
      if (test.test(modulePath)) {
        // 当前模块路径匹配到了 test 需要通过loader进行处理
        let len = use.length - 1 // 最后一项
        function normalLoader () {
          content = require(use[len--])(content)
          if (len >= 0) normalLoader()
        }
        normalLoader()
      }
      
    }
    return content
  }
  // 处理loader
  loaderHandle (content, loader) {
    return loader(content)
  }
  // 解析源码
  parse (source, parentPath) { // AST解析语法树
    let ast = babylon.parse(source)
    let dependencies = [] // 依赖的数组 某个文件的依赖数组
    traverse(ast, {
      CallExpression (p) { // 调用表达式 函数调用 例 a() require()
        let node = p.node
        if(node.callee.name === 'require') {
          // 当前的调用表达式为require
          node.callee.name = '__wzl_require__' // 改造调用名
          let moduleName = node.arguments[0].value // 取到的是模块的引用名字
          // path.extname判断是否有扩展名，没有加上.js  ./a.js
          moduleName = moduleName + (path.extname(moduleName) ? '' : 'js')
          moduleName = './' + path.join(parentPath, moduleName) // 拼接父路径'./src/js'
          dependencies.push(moduleName) // 添加依赖路径
          node.arguments = [t.stringLiteral(moduleName)] // 替换节点内容
        }
      }
    })
    let sourceCode = generator(ast).code // 转换节点
    return { sourceCode, dependencies }
  }
  // 构建模块
  buildModule (modulePath, isEntry) {
    // 拿到模块的内容
    let source = this.getSource(modulePath)
    // 模块id = modulePath - this.root (相对路径) './src/index.js'
    let moduleName = './' + path.relative(this.root, modulePath)
    isEntry && (this.entryId = moduleName) // 保存入口名字
    // 解析需要把source源码进行改造 返回一个依赖列表 path.dirname获取父路径 ./src
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
    // console.log(sourceCode, dependencies)
    // 把相对路径和模块中的内容对应起来 {'./src': sourceCode}
    this.modules[moduleName] = sourceCode
    // 递归依赖项
    dependencies.forEach(dep => {  // 附模块的加载
      this.buildModule(path.resolve(this.root,  dep), false)
    })
  }
  // 发射文件
  async emitFile () {
    // 用数据 渲染我们的
    // 拿到输出目录 this.config.output.path 必须是绝对路径
    let main = path.join(this.config.output.path, this.config.output.filename)
    // ejs模板
    let templateStr = this.getSource(path.resolve(__dirname, './main.ejs')) // 必须是绝对路径
    let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules})
    this.assets = {} // 可能是多入口
    // 资源中 路径对应的代码
    this.assets[main] = code
    await dirExists(path.dirname(main)) // 判断输出文件夹是否存在， 不存在则创建
    // 发射文件 （多入口，遍历assets）
    fs.writeFileSync(main, this.assets[main])
  }
  run () {
    this.hooks.run.call() // 执行对应的钩子
    this.hooks.compile.call()
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root,  this.entry), true)
    this.hooks.afterCompile.call()
    // console.log(this.modules, this.entryId)
    // 发射打包后的文件
    this.emitFile()
    this.hooks.emit.call()
    this.hooks.done.call()
  }
}